﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace SKBKontur.LambdaExpressionParsing
{
    public class FilterExpressionParser
    {
        internal class FilterElementBuilder
        {
            private readonly ParameterExpression parameterExpression;
            private object value;
            private readonly ConditionType conditionType;
            private PropertyInfo propertyTypeInfo;
            private object propertyParameter;

            public FilterElementBuilder(ParameterExpression expression, ExpressionType expressionType)
            {
                parameterExpression = expression;
                switch (expressionType)
                {
                    case ExpressionType.Equal:
                        conditionType = ConditionType.Equal;
                        break;
                    case ExpressionType.LessThan:
                        conditionType = ConditionType.LessThen;
                        break;
                    case ExpressionType.GreaterThan:
                        conditionType = ConditionType.GreaterThen;
                        break;
                    case ExpressionType.LessThanOrEqual:
                        conditionType = ConditionType.LessThenOrEqual;
                        break;
                    case ExpressionType.GreaterThanOrEqual:
                        conditionType = ConditionType.GreaterThenOrEqual;
                        break;
                    case ExpressionType.NotEqual:
                        throw new ParserException("Unsupported comparison operation", "!=");
                    case ExpressionType.OrElse:
                        throw new ParserException("Unsupported logical operation", "||");
                    default:
                        throw new ParserException("Unsupported operation", expressionType.ToString());
                }
            }

            private void SetValue(object value, string area)
            {
                if (this.value != null)
                    throw new ParserException("Only one hand of binary expression may contain filter value", area);
                if (value == null)
                    throw new ParserException(String.Format("Null can not be a filter value"), area);
                this.value = value;
            }

            private void SetProperty(PropertyInfo propertyInfo, string area)
            {
                if (propertyTypeInfo != null)
                    throw new ParserException(string.Format("Only one hand of binary expression may contain filter parameter"), area);
                propertyTypeInfo = propertyInfo;
            }

            private void HandleMethodCall(MethodCallExpression expression)
            {
                if (parameterExpression.Equals(expression.Object))
                    HandleParameterMethodCall(expression.Method, expression.Arguments, expression.ToString());
                else
                    SetValue(MakeExternalMethodCall(expression), CallExpressionToArea(expression));
            }

            private string CallExpressionToArea(MethodCallExpression expression)
            {
                return AccessExpressionToArea(expression.Method, expression.Object, expression.ToString());
            }

            private object MakeExternalMethodCall(MethodCallExpression expression)
            {
                var methodInfo = expression.Method;
                object obj = methodInfo.IsStatic ? null : ExtractExternalObject(expression.Object, expression.ToString());
                return methodInfo.Invoke(obj, expression.Arguments.Select(t => Evaluate(t, expression.ToString())).ToArray());
            }

            private object Evaluate(Expression expression, string area)
            {
                switch(expression.NodeType)
                {
                    case ExpressionType.Constant:
                        return ((ConstantExpression)expression).Value;
                    case ExpressionType.MemberAccess:
                        return ExtractExternalObjectFromMember((MemberExpression) expression, expression.ToString());
                    case ExpressionType.Call:
                        return MakeExternalMethodCall((MethodCallExpression)expression);
                    case ExpressionType.Parameter:
                        throw new ParserException("Parameters can not be used in this context", area);
                    default:
                        throw new ParserException(string.Format("Expected MemberAccess, Call or Constant, but was {0}", expression.NodeType), area);
                }
            }

            private object ExtractExternalObject(Expression expression, string area)
            {
                switch (expression.NodeType)
                {
                    case ExpressionType.MemberAccess:
                        return ExtractExternalObjectFromMember((MemberExpression) expression, area);
                    case ExpressionType.Constant:
                        return ((ConstantExpression) expression).Value;
                    case ExpressionType.Call:
                        return MakeExternalMethodCall((MethodCallExpression) expression);
                    default:
                        throw new ParserException(string.Format("Expected MemberAccess, Call or Constant, but was {0}", expression.NodeType), area);
                }
            }

            private object ExtractExternalObjectFromMember(MemberExpression expression, string area)
            {
                var memberInfo = expression.Member;
                switch (memberInfo.MemberType)
                {
                    case MemberTypes.Field:
                        return ExtractExternalObjectFromField((FieldInfo) memberInfo, expression.Expression, area);
                    case MemberTypes.Property:
                        return ExtractExternalObjectFromProperty((PropertyInfo) memberInfo, expression.Expression, area);
                    default:
                        throw new ParserException(string.Format("Expected Field or Property, but was {0}", memberInfo.MemberType), area);
                }
            }

            private object ExtractExternalObjectFromProperty(PropertyInfo propertyInfo, Expression expression, string area)
            {
                return propertyInfo.GetValue(Evaluate(expression, area), new object[0]);
            }

            private object ExtractExternalObjectFromField(FieldInfo fieldInfo, Expression expression, string area)
            {
                if (fieldInfo.IsStatic)
                    return fieldInfo.GetValue(null);
                return fieldInfo.GetValue(Evaluate(expression, area));
            }

            private void HandleParameterMethodCall(MethodInfo methodInfo, ReadOnlyCollection<Expression> arguments, string area)
            {
                var property = parameterExpression.Type.GetProperties().FirstOrDefault(p => p.GetGetMethod().Equals(methodInfo));
                if (property == null)
                    throw new ParserException(string.Format("Expected property getter as part of filter parameter expression, but was {0}", methodInfo), area);

                if (arguments.Count != 1)
                    throw new ParserException("Parameters with other than one arguments are not supported", String.Join(",", arguments.Select(t => t.ToString()).ToArray()));
                
                propertyParameter = Evaluate(arguments[0], area);

                SetProperty(property, arguments[0].ToString());
            }

            public FilterElement Build()
            {
                if (value == null || propertyTypeInfo == null)
                    throw new InvalidOperationException("Parser can build filter elements only after successful filter parsing");
                if (!propertyTypeInfo.PropertyType.Equals(value.GetType()))
                    CastValue();
                return new FilterElement(propertyTypeInfo, conditionType, value, propertyParameter);
            }

            private void CastValue()
            {
                var typeOfProperty = propertyTypeInfo.PropertyType;
                if (typeOfProperty.IsEnum)
                    value = Enum.Parse(typeOfProperty, value.ToString());
                else
                    throw new ParserException("Casting is not supported", value.ToString());
            }

            private void HandleConstantExpression(ConstantExpression constantExpression)
            {
                SetValue(constantExpression.Value, constantExpression.Value == null ? "null" : constantExpression.Value.ToString());
            }

            public void Handle(Expression expression)
            {
                switch (expression.NodeType)
                {
                    case ExpressionType.MemberAccess:
                        HandleMemberAccess((MemberExpression)expression);
                        break;
                    case ExpressionType.Constant:
                        HandleConstantExpression((ConstantExpression)expression);
                        break;
                    case ExpressionType.Convert:
                        HandleUnaryExpression((UnaryExpression)expression);
                        break;
                    case ExpressionType.Call:
                        HandleMethodCall((MethodCallExpression)expression);
                        break;
                    default:
                        throw new ParserException(string.Format("Expected MemberAccess, Constant or Call, but was {0}", expression.NodeType), expression.ToString());
                }
            }

            private void HandleUnaryExpression(UnaryExpression expression)
            {
                if (expression.Method != null)
                    throw new ParserException("Casting is not supported", expression.ToString());
                Handle(expression.Operand);
            }

            private void HandleMemberAccess(MemberExpression expression)
            {
                if (parameterExpression.Equals(expression.Expression))
                    HandleParameterMemberAccess(expression.Member, expression.ToString());
                else
                    SetValue(ExtractExternalObjectFromMember(expression, expression.ToString()), MemberExpressionToArea(expression));
            }

            private string MemberExpressionToArea(MemberExpression expression)
            {
                return AccessExpressionToArea(expression.Member, expression.Expression, expression.ToString());
            }

            private bool IsLinqTemporaryType(Type t)
            {
                return t.Name.StartsWith("<>");
            }

            private string AccessExpressionToArea(MemberInfo memb, Expression obj, string defaultArea)
            {
                if (obj == null) // Static member
                    return defaultArea;
                if (IsLinqTemporaryType(obj.Type))
                    return memb.Name;
                if (obj.NodeType == ExpressionType.MemberAccess)
                    return MemberExpressionToArea((MemberExpression) obj) + "." + memb.Name;
                if (obj.NodeType == ExpressionType.Call)
                    return CallExpressionToArea((MethodCallExpression) obj) + "." + memb.Name;
                return defaultArea;
            }

            private void HandleParameterMemberAccess(MemberInfo memberInfo, string area)
            {
                if (memberInfo.MemberType != MemberTypes.Property)
                    throw new ParserException(string.Format("{0}", memberInfo.Name), area);
                SetProperty((PropertyInfo) memberInfo, area);
            }
        }

        private static BinaryExpression RequireBinaryExpression(Expression expression)
        {
            Type type = expression.GetType();
            if (type != typeof(BinaryExpression))
                throw new ParserException(string.Format("Expected BinaryExpression, but was {0}", type.Name), expression.ToString());
            return (BinaryExpression) expression;
        }

        public FilterElement[] Parse<T>(Expression<Func<T, bool>> filter)
        {
            if (filter.Parameters.Count != 1)
                throw new ParserException("Expected expression with one parameter", filter.Body.ToString());
            var elements = new List<FilterElement>(ParseBinaryExpression(filter.Parameters[0], RequireBinaryExpression(filter.Body)));
            elements.Reverse();
            return elements.ToArray();
        }

        private static FilterElement BuildFilterElement(ParameterExpression expression, BinaryExpression binaryExpression)
        {
            var builder = new FilterElementBuilder(expression, binaryExpression.NodeType);
            builder.Handle(binaryExpression.Left);
            builder.Handle(binaryExpression.Right);
            return builder.Build();
        }

        private IEnumerable<FilterElement> ParseBinaryExpression(ParameterExpression expression, BinaryExpression binaryExpression)
        {
            while (binaryExpression.NodeType == ExpressionType.AndAlso)
            {
                yield return BuildFilterElement(expression, RequireBinaryExpression(binaryExpression.Right));
                binaryExpression = RequireBinaryExpression(binaryExpression.Left);
            }
            yield return BuildFilterElement(expression, binaryExpression);
        }
    }
}